/*
 *  Copyright 2006 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.servlet;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.webapp.util.core.ExternalResourceCache;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.GenericValidator;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

/**
 * <p>Caches some external resources accessible via HTTP. Also can change its
 * encoding. Caching is not obligatory: if caller wishes to suppress caching,
 * resource is loaded synchronously and returned directly. Otherwise (if cache
 * is used), then if something is in cache for this URL, this cached value is
 * returned immediately. After this a check is made whether cache needs to be
 * refreshed. If so, refresher is started asynchronously. So, in cached mode
 * caller will not have to wait while whole request-response chain is complete,
 * but data may be a little old.</p>
 * <p>URLs from which resources may be fetched are restricted. You can supply
 * two lists: list of URLs and list of prefixes. For instance, you could write
 * <pre>
 * &lt;init-param&gt;
 *     &lt;param-name&gt;urls&lt;/param-name&gt;
 *     &lt;param-value&gt;http://www.site1.com/index.html http://www.site1.com/rss/&lt;/param-value&gt;
 * &lt;/init-param&gt; 
 * &lt;init-param&gt;
 *     &lt;param-name&gt;urlPrefixes&lt;/param-name&gt;
 *     &lt;param-value&gt;http://www.site2.com&lt;/param-value&gt;
 * &lt;/init-param&gt; 
 * </pre>
 * to allow two URLs from <em>www.site1.com</em> and any URL that begins with
 * <em>http://www.site2.com</em>.
 * </p>
 * <p>Servlet accepts following parameters (through init-params):
 * <ul>
 * <li><b>urls</b> - whitespace-separated list of URLs which are allowed</li>
 * <li><b>urlPrefixes</b> - whitespace-separated list of URL prefixes; if URL
 * starts with such URL, it's allowed</li>
 * <li><b>serverCacheTime</b> - time in seconds for which content is cached
 * serverside, default is 1800</li>
 * <li><b>clientCacheTime</b> - time in seconds for which content is cached
 * clientside (by browser), default is 1800</li>
 * <li><b>fromEncoding</b> - encoding from which to convert content, default is
 * UTF-8</li>
 * <li><b>toEncoding</b> - encoding to which to convert content, default is
 * UTF-8</li>
 * </ul>
 * If <b>fromEncoding</b> and <b>toEncoding</b> are same (i.e. they are equal
 * ignoring case), content is not converted.
 * </p>
 * <p>Parameters passed through request are following:
 * <ul>
 * <li><b>url</b> - URL to fetch</li>
 * <li><b>useCache</b> - if "true", cache is used when fetching resource, else
 * it's not used</li>
 * </ul>
 * </p>
 * <p><a href="ExternalResourceCacheServlet.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.3 $ $Date: 2008/01/12 20:29:28 $
 */
public class ExternalResourceCacheServlet extends HttpServlet {

	/**
	 * Commons Logging instance.
	 */
	protected transient final Log log
            = LogFactory.getLog(ExternalResourceCacheServlet.class);

    // Default parameter values
    private static final int DEFAULT_SERVER_CACHE_TIME = 1800;
    private static final int DEFAULT_CLIENT_CACHE_TIME = 1800;
    private static final String DEFAULT_FROM_ENCODING
            = Constants.DEFAULT_ENCODING;
    private static final String DEFAULT_TO_ENCODING
            = Constants.DEFAULT_ENCODING;

    // Init parameter names
    private static final String URLS = "urls";
    private static final String URL_PREFIXES = "urlPrefixes";
    private static final String SERVER_CACHE_TIME = "serverCacheTime";
    private static final String CLIENT_CACHE_TIME = "clientCacheTime";
    private static final String FROM_ENCODING = "fromEncoding";
    private static final String TO_ENCODING = "toEncoding";

    // Request parameter names
    private static final String URL_PARAMETER = "url";
    private static final String USE_CACHE_PARAMETER = "useCache";

    /**
     * Time (in seconds) for which content is cached server-side
     */
    protected int serverCacheTime;
    /**
     * Time (in seconds) for which content is cached in browser
     */
    protected int clientCacheTime;
    /**
     * Initial content encoding
     */
    protected String fromEncoding;
    /**
     * Resulting content encoding
     */
    protected String toEncoding;

    /**
     * Set of single URLs
     */
    protected Set urls;
    /**
     * List of URL prefixes
     */
    protected List urlPrefixes;

    /**
     * Cache instance
     */
    protected ExternalResourceCache externalResourceCache;

    /**
     * Initializes the servlet.
     */
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        serverCacheTime = DEFAULT_SERVER_CACHE_TIME;
        clientCacheTime = DEFAULT_CLIENT_CACHE_TIME;
        fromEncoding = DEFAULT_FROM_ENCODING;
        toEncoding = DEFAULT_TO_ENCODING;

        this.urls = new HashSet();
        if (!GenericValidator.isBlankOrNull(config.getInitParameter(URLS))) {
            String[] urls = org.apache.commons.lang.StringUtils.split(config.getInitParameter(URLS));
            for (int i = 0; i < urls.length; i++) {
                try {
                    // Instantiating just to ensure that URL is correct
                    new URL(urls[i]);
                    this.urls.add(urls[i]);
                } catch (MalformedURLException e) {
                    throw new ServletException("Bad URL:", e);
                }
            }
        }
        if (!GenericValidator.isBlankOrNull(config.getInitParameter(URL_PREFIXES))) {
            String[] prefixes = org.apache.commons.lang.StringUtils.split(config.getInitParameter(URL_PREFIXES));
            urlPrefixes = Arrays.asList(prefixes);
        } else {
            urlPrefixes = new ArrayList(0);
        }
        if (!GenericValidator.isBlankOrNull(config.getInitParameter(SERVER_CACHE_TIME))) {
            serverCacheTime = Integer.parseInt(config.getInitParameter(SERVER_CACHE_TIME));
        }
        if (!GenericValidator.isBlankOrNull(config.getInitParameter(CLIENT_CACHE_TIME))) {
            clientCacheTime = Integer.parseInt(config.getInitParameter(CLIENT_CACHE_TIME));
        }
        if (!GenericValidator.isBlankOrNull(config.getInitParameter(FROM_ENCODING))) {
            fromEncoding = config.getInitParameter(FROM_ENCODING);
        }
        if (!GenericValidator.isBlankOrNull(config.getInitParameter(TO_ENCODING))) {
            toEncoding = config.getInitParameter(TO_ENCODING);
        }

        externalResourceCache = ExternalResourceCache.getInstance(getServletContext());
    }

    /**
     * Destroys the servlet.
     */
    public void destroy() {
        super.destroy();
    }

    /**
     * Handles the HTTP <code>GET</code> method.
     *
     * @param request  servlet request
     * @param response servlet response
     */
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    /**
     * Handles the HTTP <code>POST</code> method.
     *
     * @param request  servlet request
     * @param response servlet response
     */
    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

	/**
	 * Processes requests for both HTTP <code>GET</code> and <code>POST</code>
     * methods.
	 *
	 * @param request  servlet request
	 * @param response servlet response
     * @throws java.io.IOException if someting is wrong with i/o
	 */
    protected void processRequest(HttpServletRequest request,
                                  HttpServletResponse response)
            throws IOException {
        // first finding out if URL is allowed
        boolean urlAllowed = false;
        String requestedUrl = request.getParameter(URL_PARAMETER);
        // URL must not be null
        if (requestedUrl != null) {
            if (urls.contains(requestedUrl)) {
                // URL is one of known single URLs enlisted in init parameters:
                // allowed
                urlAllowed = true;
            } else {
                for (int i = 0; i < urlPrefixes.size(); i++) {
                    String prefix = (String) urlPrefixes.get(i);
                    if (requestedUrl.startsWith(prefix)) {
                        // URL starts with one of known prefixes: allowed
                        urlAllowed = true;
                        break;
                    }
                }
            }
        }

        if (urlAllowed) {
            boolean useCache = "true".equalsIgnoreCase(request.getParameter(USE_CACHE_PARAMETER));
            try {
                ExternalResourceCache.ResourceData resourceData
                        = externalResourceCache.loadExternalResource(requestedUrl,
                        serverCacheTime, fromEncoding, toEncoding, useCache);
                if (!processBrowserCache(resourceData, request, response)) {
                    response.setContentType(resourceData.getContentType());
                    OutputStream out = response.getOutputStream();
                    out.write(resourceData.getContent());
                    out.close();
                }
            } catch (IOException e) {
                // this can be thrown only when fetching synchronously
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        } else {
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    }

    /**
     * Returns a short description of the servlet.
     */
    public String getServletInfo() {
        return "Servlet to cache external resources converting encoding if needed";
    }

    /**
     * Sets up cache headers.
     *
     * @param resourceData Struct which contains info about cached content
     * @param request   The request being processed
     * @param response  The response being generated @return <code>true</code> if we should return SC_NOT_MODIFIED
     * @return true if cached value should be used by browser (and there's no
     * need to send content)
     */
    protected boolean processBrowserCache(ExternalResourceCache.ResourceData resourceData, HttpServletRequest request,
                                          HttpServletResponse response) {
        //ETag generation
        StringBuffer eTagBuffer = new StringBuffer("W/\"")
                .append(resourceData.getDigest()).append("\"");
        String eTag = eTagBuffer.toString();
        response.setHeader("ETag", eTag);

        boolean conditionSatisfied = false;

        int maxAge = clientCacheTime;
        if ( maxAge < 0 ) {
            response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
            response.setHeader("Pragma", "no-cache");
        } else {
            String cacheType = "public";
            response.setHeader("Cache-Control", cacheType + ",max-age="+maxAge);
            response.setHeader("Pragma", "");

            String headerValue = request.getHeader("If-None-Match");
            if (headerValue != null) {
                if (!headerValue.equals("*")) {
                    StringTokenizer commaTokenizer =
                            new StringTokenizer(headerValue, ",");
                    while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
                        String currentToken = commaTokenizer.nextToken();
                        if (currentToken.trim().equals(eTag)) {
                            conditionSatisfied = true;
                        }
                    }
                } else {
                    conditionSatisfied = true;
                }
                if (conditionSatisfied) {
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }
        }

        return conditionSatisfied;
    }
}
